/*
 * COPYRIGHT. ShenZhen JiMi Technology Co., Ltd. 2017.
 * ALL RIGHTS RESERVED.
 *
 * No part of this publication may be reproduced, stored in a retrieval system, or transmitted,
 * on any form or by any means, electronic, mechanical, photocopying, recording, 
 * or otherwise, without the prior written permission of ShenZhen JiMi Network Technology Co., Ltd.
 *
 * Amendment History:
 * 
 * Date                   By              Description
 * -------------------    -----------     -------------------------------------------
 * 2017年7月14日    li.shangzhi         Create the class
 * http://www.jimilab.com/
 */

package com.jimi.gateway.web;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLDecoder;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.RedirectStrategy;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriTemplate;

import com.netflix.config.DynamicIntProperty;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.zuul.constants.ZuulConstants;
import com.netflix.zuul.http.ServletInputStreamWrapper;

/**
 * @FileName ErrorHostRoutingUtils.java
 * @Description:
 *
 * @Date 2017年7月14日 下午2:00:12
 * @author li.shangzhi
 * @version 1.0
 */
@Component
public class ErrorHostRoutingUtils {

	private static final Logger log = LoggerFactory.getLogger(ErrorHostRoutingUtils.class);

	private static final DynamicIntProperty SOCKET_TIMEOUT = DynamicPropertyFactory.getInstance().getIntProperty(
			ZuulConstants.ZUUL_HOST_SOCKET_TIMEOUT_MILLIS, 10000);

	private static final DynamicIntProperty CONNECTION_TIMEOUT = DynamicPropertyFactory.getInstance().getIntProperty(
			ZuulConstants.ZUUL_HOST_CONNECT_TIMEOUT_MILLIS, 2000);

	private final Timer connectionManagerTimer = new Timer("ErrorHostRoutingUtils.connectionManagerTimer", true);

	private boolean sslHostnameValidationEnabled;

	private PoolingHttpClientConnectionManager connectionManager;
	private CloseableHttpClient httpClient;
	
	private static DynamicIntProperty INITIAL_STREAM_BUFFER_SIZE = DynamicPropertyFactory
			.getInstance()
			.getIntProperty(ZuulConstants.ZUUL_INITIAL_STREAM_BUFFER_SIZE, 8192);
	private ThreadLocal<byte[]> buffers = new ThreadLocal<byte[]>() {
		@Override
		protected byte[] initialValue() {
			return new byte[INITIAL_STREAM_BUFFER_SIZE.get()];
		}
	};

	private final Runnable clientloader = new Runnable() {
		@Override
		public void run() {
			try {
				ErrorHostRoutingUtils.this.httpClient.close();
			} catch (IOException ex) {
				log.error("error closing client", ex);
			}
			ErrorHostRoutingUtils.this.httpClient = newClient();
		}
	};

	@PostConstruct
	private void initialize() {
		this.httpClient = newClient();
		SOCKET_TIMEOUT.addCallback(this.clientloader);
		CONNECTION_TIMEOUT.addCallback(this.clientloader);
		this.connectionManagerTimer.schedule(new TimerTask() {
			@Override
			public void run() {
				if (ErrorHostRoutingUtils.this.connectionManager == null) {
					return;
				}
				ErrorHostRoutingUtils.this.connectionManager.closeExpiredConnections();
			}
		}, 30000, 5000);
	}

	@PreDestroy
	public void stop() {
		this.connectionManagerTimer.cancel();
	}

	public HttpResponse run(HttpServletRequest request, URL host, String uri) {
		MultiValueMap<String, String> headers = buildRequestHeaders(request);
		MultiValueMap<String, String> params = buildRequestQueryParams(request);
		String verb = getVerb(request);

		try {
			return forward(this.httpClient, host, verb, uri, request, headers, params);
//			setResponse(response);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return null;
	}

	protected PoolingHttpClientConnectionManager newConnectionManager() {
		try {
			final SSLContext sslContext = SSLContext.getInstance("SSL");
			sslContext.init(null, new TrustManager[] { new X509TrustManager() {
				@Override
				public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
				}

				@Override
				public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
				}

				@Override
				public X509Certificate[] getAcceptedIssuers() {
					return null;
				}
			} }, new SecureRandom());

			RegistryBuilder<ConnectionSocketFactory> registryBuilder = RegistryBuilder.<ConnectionSocketFactory> create().register("http",
					PlainConnectionSocketFactory.INSTANCE);
			if (this.sslHostnameValidationEnabled) {
				registryBuilder.register("https", new SSLConnectionSocketFactory(sslContext));
			} else {
				registryBuilder.register("https", new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE));
			}
			final Registry<ConnectionSocketFactory> registry = registryBuilder.build();

			this.connectionManager = new PoolingHttpClientConnectionManager(registry);
			this.connectionManager.setMaxTotal(20);
			this.connectionManager.setDefaultMaxPerRoute(5);
			return this.connectionManager;
		} catch (Exception ex) {
			throw new RuntimeException(ex);
		}
	}

	protected CloseableHttpClient newClient() {
		final RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(SOCKET_TIMEOUT.get())
				.setConnectTimeout(CONNECTION_TIMEOUT.get()).setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();

		HttpClientBuilder httpClientBuilder = HttpClients.custom();
		if (!this.sslHostnameValidationEnabled) {
			httpClientBuilder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
		}
		return httpClientBuilder.setConnectionManager(newConnectionManager()).useSystemProperties().setDefaultRequestConfig(requestConfig)
				.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false)).setRedirectStrategy(new RedirectStrategy() {
					@Override
					public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException {
						return false;
					}

					@Override
					public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context)
							throws ProtocolException {
						return null;
					}
				}).build();
	}

	private HttpResponse forward(HttpClient httpclient, URL host, String verb, String uri, HttpServletRequest request,
			MultiValueMap<String, String> headers, MultiValueMap<String, String> params) throws Exception {
		HttpHost httpHost = getHttpHost(host);
		headers.set("host", httpHost.getHostName());
		uri = StringUtils.cleanPath(uri.replaceAll("/{2,}", "/"));

		ContentType contentType = null;

		if (request.getContentType() != null) {
			contentType = ContentType.parse(request.getContentType());
		}

//		InputStreamEntity entity = new InputStreamEntity(requestEntity, contentLength, contentType);
		List<NameValuePair> list = new ArrayList<NameValuePair>();
		Enumeration<String> paramNames = request.getParameterNames();
		while(paramNames.hasMoreElements()) {
			String paramName =  paramNames.nextElement();
			if(!params.containsKey(paramName)) {
				list.add(new BasicNameValuePair(paramName, request.getParameter(paramName)));
			}
		}
		
		UrlEncodedFormEntity entity = null;
		if(contentType != null) {
			entity = new UrlEncodedFormEntity(list, contentType.getCharset());
		} else {
			entity = new UrlEncodedFormEntity(list, "UTF-8");
		}

		HttpRequest httpRequest = buildHttpRequest(verb, uri, entity, headers, params);
		try {
			log.debug(httpHost.getHostName() + " " + httpHost.getPort() + " " + httpHost.getSchemeName());
			HttpResponse zuulResponse = forwardRequest(httpclient, httpHost, httpRequest);
			return zuulResponse;
		} finally {
			// When HttpClient instance is no longer needed,
			// shut down the connection manager to ensure
			// immediate deallocation of all system resources
			// httpclient.getConnectionManager().shutdown();
		}
	}

	protected HttpRequest buildHttpRequest(String verb, String uri, UrlEncodedFormEntity entity, MultiValueMap<String, String> headers,
			MultiValueMap<String, String> params) {
		HttpRequest httpRequest;

		switch (verb.toUpperCase()) {
		case "POST":
			HttpPost httpPost = new HttpPost(uri + getQueryString(params));
			httpRequest = httpPost;
			httpPost.setEntity(entity);
			break;
		case "PUT":
			HttpPut httpPut = new HttpPut(uri + getQueryString(params));
			httpRequest = httpPut;
			httpPut.setEntity(entity);
			break;
		case "PATCH":
			HttpPatch httpPatch = new HttpPatch(uri + getQueryString(params));
			httpRequest = httpPatch;
			httpPatch.setEntity(entity);
			break;
		case "DELETE":
			BasicHttpEntityEnclosingRequest entityRequest = new BasicHttpEntityEnclosingRequest(verb, uri + getQueryString(params));
			httpRequest = entityRequest;
			entityRequest.setEntity(entity);
			break;
		default:
			httpRequest = new BasicHttpRequest(verb, uri + getQueryString(params));
			log.debug(uri + getQueryString(params));
		}

		httpRequest.setHeaders(convertHeaders(headers));
		return httpRequest;
	}

	private Header[] convertHeaders(MultiValueMap<String, String> headers) {
		List<Header> list = new ArrayList<>();
		for (String name : headers.keySet()) {
			for (String value : headers.get(name)) {
				list.add(new BasicHeader(name, value));
			}
		}
		return list.toArray(new BasicHeader[0]);
	}

	private HttpResponse forwardRequest(HttpClient httpclient, HttpHost httpHost, HttpRequest httpRequest) throws IOException {
		return httpclient.execute(httpHost, httpRequest);
	}

	private HttpHost getHttpHost(URL host) {
		HttpHost httpHost = new HttpHost(host.getHost(), host.getPort(), host.getProtocol());
		return httpHost;
	}

	private InputStream getRequestBody(HttpServletRequest request) {
		InputStream requestEntity = null;
		try {
			// Read the request body inputstream into a byte array.
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // Copy all bytes from inputstream to byte array, and record time taken.
            IOUtils.copy(request.getInputStream(), baos);

            requestEntity = new ServletInputStreamWrapper(baos.toByteArray());
		} catch (IOException ex) {
			// no requestBody is ok.
		}
		return requestEntity;
	}

	private String getVerb(HttpServletRequest request) {
		String sMethod = request.getMethod();
		return sMethod.toUpperCase();
	}

	/**
	 * @Title: builRequestHeaders
	 * @Description: 获取请求的头信息
	 * @param request
	 * @return
	 * @author li.shangzhi
	 * @date 2017年7月14日 上午11:42:31
	 */
	private MultiValueMap<String, String> buildRequestHeaders(HttpServletRequest request) {
		MultiValueMap<String, String> headers = new HttpHeaders();
		Enumeration<String> headerNames = request.getHeaderNames();
		if (headerNames != null) {
			while (headerNames.hasMoreElements()) {
				String name = headerNames.nextElement();
				if (isIncludedHeader(name)) {
					Enumeration<String> values = request.getHeaders(name);
					while (values.hasMoreElements()) {
						String value = values.nextElement();
						headers.add(name, value);
					}
				}
			}
		}
		return headers;
	}
	
	public boolean isIncludedHeader(String headerName) {
		String name = headerName.toLowerCase();
		switch (name) {
		case "host":
		case "connection":
		case "content-length":
		case "content-encoding":
		case "server":
		case "transfer-encoding":
		case "x-application-context":
			return false;
		default:
			return true;
		}
	}

	/**
	 * @Title: buildRequestQueryParams
	 * @Description: 获取请求的参数
	 * @param request
	 * @return
	 * @author li.shangzhi
	 * @date 2017年7月14日 上午11:43:02
	 */
	private MultiValueMap<String, String> buildRequestQueryParams(HttpServletRequest request) {

		Map<String, List<String>> map = getQueryParams(request);
		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
		if (map == null) {
			return params;
		}
		for (String key : map.keySet()) {
			for (String value : map.get(key)) {
				params.add(key, value);
			}
		}
		return params;
	}
	
	private Map<String, List<String>> getQueryParams(HttpServletRequest request) {
		if (request.getQueryString() == null) {
			return null;
		}
		Map<String, List<String>> qp = new HashMap<>();
		StringTokenizer st = new StringTokenizer(request.getQueryString(), "&");
		int i;

		while (st.hasMoreTokens()) {
			String s = st.nextToken();
			i = s.indexOf('=');
			if (i > 0 && s.length() >= i + 1) {
				String name = s.substring(0, i);
				String value = s.substring(i + 1);

				try {
					name = URLDecoder.decode(name, "UTF-8");
				} catch (Exception e) {
				}
				try {
					value = URLDecoder.decode(value, "UTF-8");
				} catch (Exception e) {
				}

				List<String> valueList = qp.get(name);
				if (valueList == null) {
					valueList = new LinkedList<String>();
					qp.put(name, valueList);
				}
				valueList.add(value);
			} else if (i == -1) {
				String name = s;
				String value = "";
				try {
					name = URLDecoder.decode(name, "UTF-8");
				} catch (Exception e) {
				}

				List<String> valueList = qp.get(name);
				if (valueList == null) {
					valueList = new LinkedList<String>();
					qp.put(name, valueList);
				}
				valueList.add(value);
			}
		}
		return qp;
	}

	public String getQueryString(MultiValueMap<String, String> params) {
		if (params.isEmpty()) {
			return "";
		}
		StringBuilder query = new StringBuilder();
		Map<String, Object> singles = new HashMap<>();
		for (String param : params.keySet()) {
			int i = 0;
			for (String value : params.get(param)) {
				query.append("&");
				query.append(param);
				if (!"".equals(value)) { // don't add =, if original is ?wsdl, output is not ?wsdl=
					String key = param;
					// if form feed is already part of param name double
					// since form feed is used as the colon replacement below
					if (key.contains("\f")) {
						key = (key.replaceAll("\f", "\f\f"));
					}
					// colon is special to UriTemplate
					if (key.contains(":")) {
						key = key.replaceAll(":", "\f");
					}
					key = key + i;
					singles.put(key, value);
					query.append("={");
					query.append(key);
					query.append("}");
				}
				i++;
			}
		}

		UriTemplate template = new UriTemplate("?" + query.toString().substring(1));
		return template.expand(singles).toString();
	}
	
	public void writeResponse(InputStream zin, OutputStream out) throws Exception {
		try {
			byte[] bytes = buffers.get();
			int bytesRead = -1;
			while ((bytesRead = zin.read(bytes)) != -1) {
				out.write(bytes, 0, bytesRead);
			}
		}
		catch(IOException ioe) {
			log.warn("Error while sending response to client: "+ioe.getMessage());
		}
	}
}
